Particle in a Box#
What you need to know
Particle in a Box (PIB) as a Model System
The Particle in a Box (PIB) is a simple model that helps illustrate the behavior of electrons confined within atoms and molecules. It serves as a useful tool to introduce key quantum concepts:
Energetic Quantization: Energy levels in quantum systems are discrete, not continuous, as seen in the PIB model.
Probabilistic Nature of Quantum Particles: The probability distribution for the particle’s position is non-uniform, with nodal points where the probability is zero, emphasizing the inherent uncertainty in the particle’s exact location.
Uncertainty Principle: There is an inverse relationship between the uncertainty in position and momentum, demonstrating the fundamental limit on how precisely both quantities can be known simultaneously.
Zero-Point Energy: Quantum particles always possess a minimum kinetic energy, even at absolute zero. This zero-point energy highlights the impossibility of freezing all motion in quantum systems.
Quantum-Classical Correspondence: As the system scales up, quantum behavior smoothly transitions to classical behavior, illustrating the correspondence principle.
Degeneracy of Energy Levels: In systems with symmetries, multiple wave functions can correspond to the same energy level, a phenomenon known as degeneracy.
Classical vs Quantum particle in a box#
Particle in a box is a toy model of electron (or atom, molecule, small quantum object) trapped in some region of space \([0,L]\).
The positional information of a quantum “particle” is described by a quantum wave function \(\psi(x)\) which is obtained by solving Schrödinger equation with boundary conditions
Wave functions are standing waves just like in a guitar on a string problem. With one major difference! Quantum-wave function has a probabilistic meaning and hence has a completely different meaning from a classical notion of a “wave”.
According to classical mechanis in the absence of any attractive interactions particle will be boundicng back and forth between walls with constant speed. Hence and we expect to find particle with equal probability at all locations \(x\).
According to quantum mechancis Quantum particle in the box is going to be found in some regions with high probability in others with little or zero!
Fig. 51 The particle in a box (PIB) is a convenient system for illustrating the differences between classical (A) and quantum systems (B-F). The horizontal axis is position, and the vertical axis is the real part (blue) and imaginary part (red) of the wavefunction \(\psi_n(x)\). The states (B,C,D) are the eigenfunctions of Hamiltonian \(n=1,2,3\). While E and F are not.#
Solving the Schrödinger Equation for the Particle in a Box (PIB)#
Fig. 52 Particle in a box subject to infinitely high potential walls#
The Schrödinger equation for a particle in a box (PIB) is defined by a Hamiltonian operator that incorporates the potential energy, which is infinitely large at the boundaries of the box and zero inside. This potential ensures that the particle is confined within the box, where it can only possess kinetic energy.
The potential energy for PIB is defined:
The boundary conditions are:
The Hamiltonian operator is in this case accounting only for kinetic energy!
Now, we have all the necessary ingredients to solve the time-independent Schrödinger equation for the 1D PIB:
Substituting the Hamiltonian, we get:
where the \(k^2\) is a positive real number that relates the particle’s energy \(E\) to its wavefunction.
Solution and Boundary Conditions#
Mathematically, the form of the 1D PIB problem is similar to the ordinary differential equation (ODE) used in the 1D vibrating guitar string problem. The key differences lie in the constant coefficients and the interpretation of the wavefunction.
The general solution to this differential equation is:
Applying the boundary condition \(\psi(0) = 0\), we find that \(A = 0\), leaving us with:
Applying the boundary condition \(\psi(L) = 0\), we get:
This condition is satisfied when:
Thus, the wavefunction becomes:
Using the relationship \(k^2 = \frac{n^2\pi^2}{L^2} = \frac{2mE}{\hbar^2}\), we can express the energy levels as:
The quantization of energy results from confining the wavefunction within a finite space. This is the reason bound states exhibit quantized energy levels. Atoms, molecules, and solids all possess discrete energy levels due to similar constraints.
Wavefunctions Must Be Normalized#
Next, we determine the constant coefficient \(B_n\) by enforcing the normalization condition:
To evaluate the integral, we use the trigonometric identity \(\sin^2(x) = \frac{1}{2}(1 - \cos(2x))=1\)
Since the integral of \(\cos\left(\frac{2n\pi x}{L}\right)\) over a full period from \(0\) to \(L\) is zero, we are left with:
Solving for \(B_n\) we determine normalization constant which ensures that square of wavefunction integrates to 1.
PIB Eigenfunctions and Eigenvalues#
Eigenfunctions and eigenvalues of 1D particle in a box
Full time dependent solution
Where the coefficients \(c_n\) depend on initial condition, e.g could be all zero except one (pure state) or a few of them non-zero (mixed state)
Show code cell source
import numpy as np
import matplotlib.pyplot as plt
# Parameters
L = 1.0 # Length of the box
x = np.linspace(0, L, 1000) # Position array
n_max = 5 # Maximum quantum number to display
# Constants
hbar = 1.0545718e-34 # Reduced Planck's constant (J·s)
m = 9.10938356e-31 # Mass of an electron (kg)
# Functions
def psi_n(n, x, L):
"""Wavefunction for a particle in a 1D box."""
return np.sqrt(2 / L) * np.sin(n * np.pi * x / L)
def psi_n_squared(n, x, L):
"""Probability density (wavefunction squared)."""
return psi_n(n, x, L) ** 2
def energy_levels(n, L):
"""Energy level for the particle in the box."""
return (n**2 * np.pi**2 * hbar**2) / (2 * m * L**2)
# Energy formula string
energy_formula = r"$E_n = \frac{n^2 \pi^2 \hbar^2}{2mL^2}$"
# Plotting
fig, axs = plt.subplots(1, 2, figsize=(8, 6), sharey=True)
# Plot wavefunctions in the first subplot
for n in range(1, n_max + 1):
wavefunction = psi_n(n, x, L)
axs[0].plot(x, wavefunction + n**2, label=f'n={n}')
axs[0].hlines(n**2, 0, L, colors='gray', linestyles='dashed') # Energy level lines
# Wavefunction plot labels and title
axs[0].set_xlabel('Position (x)', fontsize=12)
axs[0].set_ylabel(r'$\psi_n(x)$', fontsize=12)
axs[0].legend(loc='upper right', bbox_to_anchor=(1.15, 1))
# Plot wavefunction squared (probability densities) in the second subplot
for n in range(1, n_max + 1):
wavefunction_squared = psi_n_squared(n, x, L)
axs[1].plot(x, wavefunction_squared + n**2, label=f'n={n}')
axs[1].hlines(n**2, 0, L, colors='gray', linestyles='dashed') # Energy level lines
# Probability density plot labels
axs[1].set_xlabel('Position (x)', fontsize=12)
axs[1].set_ylabel(r'$|\psi_n(x)|^2$', fontsize=12)
# Display the energy formula and levels in the second subplot
for n in range(1, n_max + 1):
energy_value = energy_levels(n, L)
axs[1].text(L, n**2, f"$E_{n}$ = {energy_value:.2e} J", verticalalignment='bottom', fontsize=12)
# Overall title for the figure
fig.suptitle(f'Wavefunctions and Energies for a 1D Particle in a Box (n=1 to n={n_max})')
# Make room for labels outside plot and improve layout
plt.tight_layout(rect=[0, 0, 1, 0.95])
plt.show()
Discrete energy levels and zero point energy#
Quantum particles are never at rest
The lowest energy electon in a box can have is at \(n=1\) at which is not zero!
Keeping in mind that the energy is purely kinetic, this means quantum particles never ceases their motion.
The spacing of energy levels is finite and depends on the size of the box!
Increasing Box size leads to more classical behavior
As the box size is increased energy spacing gets smaller!
Thus quantum effects are more pronounced when electorn is bound in smaller regions of space.
Non-uniform probabilities and nodes#
There are \(n-1\) nodes for quantum state \(n\)
Recall that sine function hitz zero at integer values of \(\pi\), \(sin(n\pi)=0\) when \(n=1,2,3,4,...\)
The wavefunction \(\psi_n(x) = sin \frac{n\pi x}{L}\) will then have \(n-1\) nodes at points \(x=L/n\), \(2L/n\), \(3L/n\), … \((n-1)L/n\).
Note that we do not count \(x=0\) and \(x=L\) points as nodes because they are part of boudnary conditions that apply to all wavefunctions
For instance \(n=3\) nodes are at \(x=L/3\), \(x=2L/3\).
For instance \(n=4\) nodes are at \(L/4\), \(2L/4\), \(3L/4\)
Nodes imply zero probability to find an electron in the box.
\(|\psi_n|^2=0\) at nodal points which means zero probability.
This is in sharp contradiction of classical mechanics which bases its prediction on purely particle like motion of electron.
Existence of nodes implies a wave like character of electron
On time-dependence#
Pure states#
Pure states correspond to time-independent probability distribution.
Below we visualize a few of the wavefunction at its square for \(n=2\).
Show code cell source
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
from IPython.display import HTML
# Parameters
L = 1.0 # Length of the box
hbar = 1.0 # Set hbar = 1 for simplicity
m = 1.0 # Set mass = 1 for simplicity
# Define spatial and time arrays
x = np.linspace(0, L, 200)
t = np.linspace(0, 10, 200)
n = 2
# Define energy for nth state
def energy_n(n, L):
return (n**2 * np.pi**2 * hbar**2) / (2 * m * L**2)
# Define wavefunction for nth state
def psi_total(n, x, L, t):
E_n = energy_n(n, L)
return np.sqrt(2 / L) * np.sin(n * np.pi * x / L) * np.exp(-1j * E_n * t / hbar)
# Set up the figure and axis
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(11, 8))
# Set limits for the wavefunction and wavefunction squared
ax1.set_xlim(0, L)
ax1.set_ylim(-2, 2)
ax2.set_xlim(0, L)
ax2.set_ylim(0, 4)
# Set labels for the wavefunction plot
ax1.set_xlabel('Position (x)', fontsize=14)
ax1.set_ylabel(r'Re[$\psi(x,t)$]', fontsize=14)
ax1.set_title('Time Evolution of Wavefunction', fontsize=16)
# Set labels for the squared wavefunction plot
ax2.set_xlabel('Position (x)', fontsize=14)
ax2.set_ylabel(r'$|\psi(x,t)|^2$', fontsize=14)
ax2.set_title('Probability Density', fontsize=16)
### Animation settings
# Initialize the wavefunction and its square plots
line_real, = ax1.plot([], [], lw=2)
line_prob, = ax2.plot([], [], lw=2)
# Initialization function for the plot
def init():
line_real.set_data([], [])
line_prob.set_data([], [])
return line_real, line_prob
# Animation function which updates the plot each frame
def animate(i):
t_val = t[i]
psi = psi_total(n, x, L, t_val)
psi_real = np.real(psi)
psi_squared = np.abs(psi)**2
line_real.set_data(x, psi_real)
line_prob.set_data(x, psi_squared)
return line_real, line_prob
# Create the animation
ani = FuncAnimation(fig, animate, init_func=init, frames=len(t), interval=150, blit=True)
plt.close(fig) # Prevents static display of the last frame
HTML(ani.to_jshtml())
---------------------------------------------------------------------------
KeyboardInterrupt Traceback (most recent call last)
Cell In[2], line 72
69 ani = FuncAnimation(fig, animate, init_func=init, frames=len(t), interval=150, blit=True)
71 plt.close(fig) # Prevents static display of the last frame
---> 72 HTML(ani.to_jshtml())
File /opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/matplotlib/animation.py:1352, in Animation.to_jshtml(self, fps, embed_frames, default_mode)
1348 path = Path(tmpdir, "temp.html")
1349 writer = HTMLWriter(fps=fps,
1350 embed_frames=embed_frames,
1351 default_mode=default_mode)
-> 1352 self.save(str(path), writer=writer)
1353 self._html_representation = path.read_text()
1355 return self._html_representation
File /opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/matplotlib/animation.py:1107, in Animation.save(self, filename, writer, fps, dpi, codec, bitrate, extra_args, metadata, extra_anim, savefig_kwargs, progress_callback)
1105 progress_callback(frame_number, total_frames)
1106 frame_number += 1
-> 1107 writer.grab_frame(**savefig_kwargs)
File /opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/matplotlib/animation.py:767, in HTMLWriter.grab_frame(self, **savefig_kwargs)
765 return
766 f = BytesIO()
--> 767 self.fig.savefig(f, format=self.frame_format,
768 dpi=self.dpi, **savefig_kwargs)
769 imgdata64 = base64.encodebytes(f.getvalue()).decode('ascii')
770 self._total_bytes += len(imgdata64)
File /opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/matplotlib/figure.py:3378, in Figure.savefig(self, fname, transparent, **kwargs)
3374 for ax in self.axes:
3375 stack.enter_context(
3376 ax.patch._cm_set(facecolor='none', edgecolor='none'))
-> 3378 self.canvas.print_figure(fname, **kwargs)
File /opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/matplotlib/backend_bases.py:2366, in FigureCanvasBase.print_figure(self, filename, dpi, facecolor, edgecolor, orientation, format, bbox_inches, pad_inches, bbox_extra_artists, backend, **kwargs)
2362 try:
2363 # _get_renderer may change the figure dpi (as vector formats
2364 # force the figure dpi to 72), so we need to set it again here.
2365 with cbook._setattr_cm(self.figure, dpi=dpi):
-> 2366 result = print_method(
2367 filename,
2368 facecolor=facecolor,
2369 edgecolor=edgecolor,
2370 orientation=orientation,
2371 bbox_inches_restore=_bbox_inches_restore,
2372 **kwargs)
2373 finally:
2374 if bbox_inches and restore_bbox:
File /opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/matplotlib/backend_bases.py:2232, in FigureCanvasBase._switch_canvas_and_return_print_method.<locals>.<lambda>(*args, **kwargs)
2228 optional_kws = { # Passed by print_figure for other renderers.
2229 "dpi", "facecolor", "edgecolor", "orientation",
2230 "bbox_inches_restore"}
2231 skip = optional_kws - {*inspect.signature(meth).parameters}
-> 2232 print_method = functools.wraps(meth)(lambda *args, **kwargs: meth(
2233 *args, **{k: v for k, v in kwargs.items() if k not in skip}))
2234 else: # Let third-parties do as they see fit.
2235 print_method = meth
File /opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/matplotlib/backends/backend_agg.py:509, in FigureCanvasAgg.print_png(self, filename_or_obj, metadata, pil_kwargs)
462 def print_png(self, filename_or_obj, *, metadata=None, pil_kwargs=None):
463 """
464 Write the figure to a PNG file.
465
(...)
507 *metadata*, including the default 'Software' key.
508 """
--> 509 self._print_pil(filename_or_obj, "png", pil_kwargs, metadata)
File /opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/matplotlib/backends/backend_agg.py:457, in FigureCanvasAgg._print_pil(self, filename_or_obj, fmt, pil_kwargs, metadata)
452 def _print_pil(self, filename_or_obj, fmt, pil_kwargs, metadata=None):
453 """
454 Draw the canvas, then save it using `.image.imsave` (to which
455 *pil_kwargs* and *metadata* are forwarded).
456 """
--> 457 FigureCanvasAgg.draw(self)
458 mpl.image.imsave(
459 filename_or_obj, self.buffer_rgba(), format=fmt, origin="upper",
460 dpi=self.figure.dpi, metadata=metadata, pil_kwargs=pil_kwargs)
File /opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/matplotlib/backends/backend_agg.py:400, in FigureCanvasAgg.draw(self)
396 # Acquire a lock on the shared font cache.
397 with RendererAgg.lock, \
398 (self.toolbar._wait_cursor_for_draw_cm() if self.toolbar
399 else nullcontext()):
--> 400 self.figure.draw(self.renderer)
401 # A GUI class may be need to update a window using this draw, so
402 # don't forget to call the superclass.
403 super().draw()
File /opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/matplotlib/artist.py:95, in _finalize_rasterization.<locals>.draw_wrapper(artist, renderer, *args, **kwargs)
93 @wraps(draw)
94 def draw_wrapper(artist, renderer, *args, **kwargs):
---> 95 result = draw(artist, renderer, *args, **kwargs)
96 if renderer._rasterizing:
97 renderer.stop_rasterizing()
File /opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/matplotlib/artist.py:72, in allow_rasterization.<locals>.draw_wrapper(artist, renderer)
69 if artist.get_agg_filter() is not None:
70 renderer.start_filter()
---> 72 return draw(artist, renderer)
73 finally:
74 if artist.get_agg_filter() is not None:
File /opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/matplotlib/figure.py:3175, in Figure.draw(self, renderer)
3172 # ValueError can occur when resizing a window.
3174 self.patch.draw(renderer)
-> 3175 mimage._draw_list_compositing_images(
3176 renderer, self, artists, self.suppressComposite)
3178 for sfig in self.subfigs:
3179 sfig.draw(renderer)
File /opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/matplotlib/image.py:131, in _draw_list_compositing_images(renderer, parent, artists, suppress_composite)
129 if not_composite or not has_images:
130 for a in artists:
--> 131 a.draw(renderer)
132 else:
133 # Composite any adjacent images together
134 image_group = []
File /opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/matplotlib/artist.py:72, in allow_rasterization.<locals>.draw_wrapper(artist, renderer)
69 if artist.get_agg_filter() is not None:
70 renderer.start_filter()
---> 72 return draw(artist, renderer)
73 finally:
74 if artist.get_agg_filter() is not None:
File /opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/matplotlib/axes/_base.py:3028, in _AxesBase.draw(self, renderer)
3025 for spine in self.spines.values():
3026 artists.remove(spine)
-> 3028 self._update_title_position(renderer)
3030 if not self.axison:
3031 for _axis in self._axis_map.values():
File /opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/matplotlib/axes/_base.py:2972, in _AxesBase._update_title_position(self, renderer)
2970 top = max(top, bb.ymax)
2971 if title.get_text():
-> 2972 ax.yaxis.get_tightbbox(renderer) # update offsetText
2973 if ax.yaxis.offsetText.get_text():
2974 bb = ax.yaxis.offsetText.get_tightbbox(renderer)
File /opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/matplotlib/axis.py:1337, in Axis.get_tightbbox(self, renderer, for_layout_only)
1334 renderer = self.figure._get_renderer()
1335 ticks_to_draw = self._update_ticks()
-> 1337 self._update_label_position(renderer)
1339 # go back to just this axis's tick labels
1340 tlb1, tlb2 = self._get_ticklabel_bboxes(ticks_to_draw, renderer)
File /opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/matplotlib/axis.py:2623, in YAxis._update_label_position(self, renderer)
2620 except KeyError:
2621 # use Axes if spine doesn't exist
2622 spinebbox = self.axes.bbox
-> 2623 bbox = mtransforms.Bbox.union(bboxes + [spinebbox])
2624 left = bbox.x0
2625 self.label.set_position(
2626 (left - self.labelpad * self.figure.dpi / 72, y)
2627 )
File /opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/matplotlib/transforms.py:658, in BboxBase.union(bboxes)
656 y0 = np.min([bbox.ymin for bbox in bboxes])
657 y1 = np.max([bbox.ymax for bbox in bboxes])
--> 658 return Bbox([[x0, y0], [x1, y1]])
File /opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/matplotlib/transforms.py:767, in Bbox.__init__(self, points, **kwargs)
764 raise ValueError('Bbox points must be of the form '
765 '"[[x0, y0], [x1, y1]]".')
766 self._points = points
--> 767 self._minpos = np.array([np.inf, np.inf])
768 self._ignore = True
769 # it is helpful in some contexts to know if the bbox is a
770 # default or has been mutated; we store the orig points to
771 # support the mutated methods
KeyboardInterrupt:
Mixed states#
Mixed states show time dependence. Below we visualize lienar combination of the first two eigenfunctions of PIB.
Step-by-Step illustration of time-dependence
The complex conjugate of \(\psi(x,t)\) is:
The squared wavefunction \(|\psi(x,t)|^2\) is:
Expanding the product:
Final Expression
This consists of:
The probability densities of \(\psi_1(x)\) and \(\psi_2(x)\) with coefficients \(|c_1|^2\) and \(|c_2|^2\).
A time-dependent interference term that oscillates with the frequency \((E_1 - E_2)/\hbar\).
Show code cell source
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
from IPython.display import HTML
# Parameters
L = 1.0 # Length of the box
hbar = 1.0 # Set hbar = 1 for simplicity
m = 1.0 # Set mass = 1 for simplicity
n_states = [1, 2] # Quantum numbers for the superposition
coeffs = [1, 1] # Coefficients for the superposition
coeffs = np.array(coeffs) / np.sqrt(np.sum(np.array(coeffs) ** 2)) # Normalize coefficients
# Define spatial and time arrays
x = np.linspace(0, L, 200)
t = np.linspace(0, 10, 200)
# Define wavefunction for nth state
def psi_n(n, x, L):
return np.sqrt(2 / L) * np.sin(n * np.pi * x / L)
# Define energy for nth state
def energy_n(n, L):
return (n**2 * np.pi**2 * hbar**2) / (2 * m * L**2)
# Define time-dependent wavefunction
def psi_total(x, t, L, coeffs, n_states):
psi_t = np.zeros_like(x, dtype=complex)
for idx, n in enumerate(n_states):
psi_x = psi_n(n, x, L)
E_n = energy_n(n, L)
time_factor = np.exp(-1j * E_n * t / hbar)
psi_t += coeffs[idx] * psi_x * time_factor
return psi_t
# Set up the figure and axis
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(11, 8))
# Set limits for the wavefunction and wavefunction squared
ax1.set_xlim(0, L)
ax1.set_ylim(-2, 2)
ax2.set_xlim(0, L)
ax2.set_ylim(0, 4)
# Set labels for the wavefunction plot
ax1.set_xlabel('Position (x)', fontsize=14)
ax1.set_ylabel(r'Re[$\psi(x,t)$]', fontsize=14)
ax1.set_title('Time Evolution of Wavefunction', fontsize=16)
# Set labels for the squared wavefunction plot
ax2.set_xlabel('Position (x)', fontsize=14)
ax2.set_ylabel(r'$|\psi(x,t)|^2$', fontsize=14)
ax2.set_title('Probability Density', fontsize=16)
### Animation settings
# Initialize the wavefunction and its square plots
line_real, = ax1.plot([], [], lw=2)
line_prob, = ax2.plot([], [], lw=2)
# Initialization function for the plot
def init():
line_real.set_data([], [])
line_prob.set_data([], [])
return line_real, line_prob
# Animation function which updates the plot each frame
def animate(i):
t_val = t[i]
psi = psi_total(x, t_val, L, coeffs, n_states)
psi_real = np.real(psi)
psi_squared = np.abs(psi)**2
line_real.set_data(x, psi_real)
line_prob.set_data(x, psi_squared)
return line_real, line_prob
# Create the animation
ani = FuncAnimation(fig, animate, init_func=init, frames=len(t), interval=150, blit=True)
plt.close(fig) # Prevents static display of the last frame
HTML(ani.to_jshtml())